/*
 * mm.c
 * 
 * Manejador de Memoria
 *  
 */

#include <mm.h>
#include <om.h>
#include <vga.h>
#include <i386.h>
#include <debug.h>
#include <string.h>

extern void* _end; 		// Puntero al final del codigo de kernel.bin (definido por LD)

/* Page Directory del Kernel */

mm_page_entry kernel_pd[MM_PD_ENTRIES] __attribute__((aligned(MM_PAGE_SIZE))) = { MM_NULL_ENTRY };


/* Lista de paginas libres de kernel y usuario */

mm_page_node *kernel_free_pages = NULL;
mm_page_node *user_free_pages   = NULL;

/* Cantidad de memoria RAM en KB */

static uint_32 kb_ram = 0;

static bool mm_valid_physical_page(void* phaddr);

static void mm_init_kernel_free_list();
static void mm_init_user_free_list();

static void mm_init_pd();

static void mm_activate_pagination();
static void mm_return_page(void *phaddr, mm_page_node **free_page_list_ptr);

static mm_page_node *mm_get_free_page(mm_page_node** free_page_list_ptr);

static void mm_debug_free_list(mm_page_node *page_node_list);

/**
 * Funcion que inicializa las estructuras del manejador de memoria. **/

void mm_init()
{
    /* Primero veo de tener suficiente memoria para ejecutar el SO, 
    * esto es, OM_KERNEL_MEMORY_LIMIT para el kernel y una pagina para usuario */
    
    if(!mm_valid_physical_page(OM_KERNEL_MEMORY_LIMIT))
        debug_kernelpanic_custom("\n  El sistema operativo no tiene suficiente memoria fisica para ejecutar!");
    
    /* Si sigo por aca es porque hay suficiente memoria */
    
    mm_init_kernel_free_list();
    mm_init_user_free_list();
    
    mm_init_pd(&kernel_pd);
    
    mm_activate_pagination();
}

/**
 * Solicita una nueva pagina de usuario.
 * 
 * @return
 * 
 * Si existen paginas libres, devuelve la direccion fisica de la pagina.
 * Caso contrario devuelve NULL.
 * 
 **/

void *mm_mem_alloc()
{
    return mm_get_free_page(&user_free_pages);
}

/**
 * Solicita una nueva pagina de kernel.
 * 
 * @return
 * 
 * Si existen paginas libres, devuelve la direccion fisica de la pagina.
 * Caso contrario devuelve NULL.
 * 
 **/

void *mm_mem_kalloc()
{
    return mm_get_free_page(&kernel_free_pages);
}

/**
 * Libera la pagina asociada a la direccion fisica pasada como 
 * parametro. La pagina puede ser tanto de kernel como de usuario.
 * 
 * @param phaddr
 * 
 * Direccion de la pagina a liberar
 * 
 **/

void mm_mem_free(void *phaddr)
{
    vga_printf("mm_mem_free %h\n", (uint_32 *) phaddr);
    
    if (phaddr < OM_KERNEL_MEMORY_LIMIT)
        mm_return_page(phaddr, &kernel_free_pages);
    else
        mm_return_page(phaddr, &user_free_pages);
}

/**
 * Inicializa un nuevo directorio de paginas con el mapa de memoria 
 * del kernel
 * 
 * @return
 * 
 * Puntero al directorio de paginas creado.
 * 
 **/

mm_page_entry *mm_dir_new()
{
    mm_page_entry *page_dir = mm_mem_kalloc();
    mm_init_pd(page_dir);
    
    return page_dir;
}

/**
 * Borra el Page Directory pasado como parametro, liberando la 
 * pagina en la que se encuentra y todas las paginas asociadas a 
 * los Page Tables que contenga.
 * 
 **/

void mm_dir_free(uint_32 *page_dir)
{
    /* La primera entrada del Page Directory direcciona los primeros 
     * 4 MB de memoria, las cuales no deben liberarse porque 
     * no corresponden con paginas que puedan ser pedidas */
    
    mm_mem_free(MM_ATTR_PDE_PADDR(page_dir[0]));
    
    int i;
    for (i = 1; i < MM_PD_ENTRIES; i++)
    {
        if (page_dir[i] & MM_ATTR_PDE_P) /* Si la entrada esta presente */
        {
            uint_32 *page_table = MM_ATTR_PDE_PADDR(page_dir[i]);
            
            int j;
            for (j = 0; j < MM_PT_ENTRIES; j++)
            {
                if(page_table[j] & MM_ATTR_PTE_P) /* Si la entrada esta presente */
                {
                    void *page = MM_ATTR_PTE_PADDR(page_table[j]);
                    mm_mem_free(page); 
                }
            }
            
            mm_mem_free(page_table);
        }
    }
    
    mm_mem_free(page_dir);
}

/**
 * Retorna la cantidad de memoria RAM detectada en el sistema en KB.
 * La deteccion de la cantidad de memoria RAM se realiza en mm_init()
 * 
 **/

uint_32 mm_ram_amount()
{
    return kb_ram;
}

/**
 * Funciones para debuggear
 *
 **/

void mm_debug_kernel_free_pages()
{
    mm_debug_free_list(kernel_free_pages);
}

void mm_debug_user_free_pages()
{
    mm_debug_free_list(user_free_pages);
}

/**********************************************************************/
/***                   FUNCIONES NO EXPORTADAS                      ***/
/**********************************************************************/

/**
 * Inicializa la lista de paginas libres de kernel. 
 * 
 * Los nodos de la lista estan ubicado al comienzo de cada pagina libre,
 * y apuntan al nodo anterior y al nodo siguiente en la lista **/

static void mm_init_kernel_free_list()
{
	/* El primer nodo lo hago aparte */
	
	mm_page_node *first = (mm_page_node*) OM_KERNEL_PAGES_START;
    
    first->cant_ref = 0;    
    first->prev = NULL;
    first->next = NULL;
    
    kernel_free_pages = first;    
        
    /* Completo el resto */
    
    mm_page_node *prev = first;
    
    void *phaddr;        
    for
    (   phaddr = OM_KERNEL_PAGES_START + MM_PAGE_SIZE;
        phaddr < OM_KERNEL_MEMORY_LIMIT;
        phaddr += MM_PAGE_SIZE
    )
    {
        mm_page_node *actual = (mm_page_node*) phaddr;
        
		/* Linkeo los nodos */
        
		actual->cant_ref = 0;
		actual->prev = prev;
		actual->next = NULL;
        
		prev->next = actual;
		prev = actual;
	}
}

/**
 * Inicializa la lista de paginas libres de usuario y calcula la cantidad 
 * de total de memoria fisica del sistema.
 * 
 * La estructura de la lista es la misma que se usa para la del kernel **/

static void mm_init_user_free_list()
{
	uint_32 cant_paginas = 0;
	
	/* El primer nodo lo hago aparte */
		
	mm_page_node *first = (mm_page_node*) OM_USER_MEMORY_START;
    
    first->cant_ref = 0;    
    first->prev = NULL;
    first->next = NULL;
    
    user_free_pages = first;
    
    /* Completo el resto y calculo la cantidad de memoria */
    
    mm_page_node *prev = first;	
	
	void *phaddr;
	for
	(
		phaddr = OM_USER_MEMORY_START + MM_PAGE_SIZE;
		mm_valid_physical_page(phaddr) && phaddr != NULL;
		phaddr += MM_PAGE_SIZE
	)
	{
		mm_page_node *actual = (mm_page_node*) phaddr;
        
		/* Linkeo los nodos */
        
		actual->cant_ref = 0;
		actual->prev = prev;
		actual->next = NULL;
        
		prev->next = actual;
		prev = actual;
        
		cant_paginas++;
    }
    
    kb_ram = (((uint_32) OM_USER_MEMORY_START) / 0x400) + (cant_paginas + 1) * 4;
}

/**
 * Inicializa el Page Directory pasado como parametro, 
 * mapeando los primeros 4 MB de memoria con identity mapping, 
 * excepto la primera pagina. **/ 

static void mm_init_pd(mm_page_entry pd[])
{
    /* Mapeo con identity mapping toda la memoria del kernel, excepto la primera pagina */
    
    memset((void*) pd, 0, MM_PAGE_SIZE);

    void *addr;
    for
    (
        addr = OM_KERNEL_MEMORY_START + MM_PAGE_SIZE;
        addr < OM_KERNEL_MEMORY_LIMIT;
        /* Vacio */
    )
    {
        /* Creo una nueva Page Table */
        
        mm_page_entry *pt = mm_mem_kalloc();
        
        uint_32 pd_index = MM_PD_INDEX(addr);
        kernel_pd[pd_index] = MM_MAKE_ENTRY(pt, MM_ATTR_PDE_P | MM_ATTR_PDE_WRITE | MM_ATTR_PDE_PWT | MM_ATTR_PDE_SUPERV);
        
        memset((void*) pt, 0, MM_PAGE_SIZE);
        
        /* La completo */
        
        int i;
        for
        (
            /* Vacio*/ ;
            addr < OM_KERNEL_MEMORY_LIMIT && pd_index == MM_PD_INDEX(addr);		// Mientras no cambie de indice en el PD
            addr += MM_PAGE_SIZE, i++
        )
        {
            uint_32 pt_index = MM_PT_INDEX(addr);
            pt[pt_index] = MM_MAKE_ENTRY(addr, MM_ATTR_PTE_P | MM_ATTR_PTE_WRITE | MM_ATTR_PTE_PWT | MM_ATTR_PTE_SUPERV);
        }
    }
}

/**
 * Determina si existe la direccion fisica de memoria pasada como parametro.
 * 
 * En realidad determina si el marco de pagina (de 4 KB) que contiene a la direccion 
 * pasada como parametro existe en memoria. 
 * 
 * @param phaddr
 * 
 * Direccion fisica de memoria a determinar si existe o no.
 * 
 * @return
 * 
 * NULL si el marco de pagina no existe.
 * Distinto de 0 en caso contrario.
 * 
 **/

static bool mm_valid_physical_page(void *phaddr)
{
    volatile uint_32* addr = MM_ALIGN_TO_PAGE_START(phaddr);    
    *addr = 0xF0F0F0F0;
    
    // Si lo que guarde en la direccion no es lo mismo que estoy leyendo, entonces la direccion no existe

    return *addr == 0xF0F0F0F0;	
}

/**
 * Activa paginacion **/

static void mm_activate_pagination()
{
	/* Cargo el registro CR3 */
	
    lcr3((uint32_t)kernel_pd);
    
    /* Activo paginacion */
    
    uint32_t cr0 = rcr0() | CR0_PG;
    lcr0(cr0);
}

/**
 * Obtiene una pagina libre de la lista pasada como parametro.
 * 
 * @param free_page_list_ptr
 * 
 * Puntero a la lista de paginas.
 * 
 * @return
 * 
 * Si existe pagina libre devuelve el puntero a la misma.
 * Caso contrario retorna NULL.
 * 
 **/

static mm_page_node *mm_get_free_page(mm_page_node **free_page_list_ptr)
{
    if (!*free_page_list_ptr)
        return NULL;
    
    /* Si la lista no esta vacia, agarro la primera pagina libre y apunto al nuevo principio */
     
    mm_page_node *first = *free_page_list_ptr;
        
    *free_page_list_ptr = first->next;
    
    /* Si la lista no queda vacia, arreglo el previo del nuevo primero */
    
    if (!*free_page_list_ptr)
        first->next->prev = NULL;

    return first;    
}

/**
 * Retorna la pagina correspondiente de la direccion phaddr 
 * a la lista de paginas libres apuntada por free_page_list_ptr
 * 
 * @param phaddr
 * 
 * La direccion de la pagina que se quiere liberar.
 * 
 * @param free_page_list_ptr
 * 
 * Puntero a la lista de paginas libres donde se guardara la pagina a liberar.
 * 
 **/

static void mm_return_page(void *phaddr, mm_page_node **free_page_list_ptr)
{
    mm_page_node *page_node = (mm_page_node *) MM_ALIGN_TO_PAGE_START(phaddr);
    mm_page_node *first = *free_page_list_ptr;
    
    if (!first) /* La lista esta vacia */
    {
        *free_page_list_ptr = page_node;
        
        page_node->cant_ref = 0;
        page_node->prev = NULL;
        page_node->next = NULL;
    }
    else if (page_node < first) /* Hay un nuevo primero */
    {
        first->prev = page_node;
        
        page_node->cant_ref = 0;
        page_node->prev = NULL;
        page_node->next = first;
        
        *free_page_list_ptr = page_node;
    }
    else /* Tengo que insertar el nodo por la mitad o al final */
    {
        mm_page_node *prev_node, *next_node;
        for
        (   prev_node = first, next_node = first->next;
            next_node;
            prev_node = next_node, next_node = next_node->next
        )
        ;   /* Nothing */
        
        page_node->cant_ref = 0;
        page_node->prev = prev_node;
        page_node->next = next_node;
    }
}

/**
 * Debug de una lista de paginas libres
 * 
 **/
 
static void mm_debug_free_list(mm_page_node *page_node_list)
{
    mm_page_node *actual;
    for
    (
        actual = page_node_list;
        actual;
        actual = actual->next
    )
    {
        vga_printf("Node at: %h\n", actual);
        breakpoint();
    }
}
